1 /*
2 * Copyright 2002-2013 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.springframework.beans.factory.config;
18
19 import java.lang.reflect.Constructor;
20 import java.lang.reflect.InvocationHandler;
21 import java.lang.reflect.Method;
22 import java.lang.reflect.Proxy;
23 import java.util.Properties;
24
25 import org.springframework.beans.BeanUtils;
26 import org.springframework.beans.BeansException;
27 import org.springframework.beans.FatalBeanException;
28 import org.springframework.beans.factory.BeanFactory;
29 import org.springframework.beans.factory.BeanFactoryAware;
30 import org.springframework.beans.factory.FactoryBean;
31 import org.springframework.beans.factory.InitializingBean;
32 import org.springframework.beans.factory.ListableBeanFactory;
33 import org.springframework.util.ReflectionUtils;
34 import org.springframework.util.StringUtils;
35
36 /**
37 * A {@link FactoryBean} implementation that takes an interface which must have one or more
38 * methods with the signatures {@code MyType xxx()} or {@code MyType xxx(MyIdType id)}
39 * (typically, {@code MyService getService()} or {@code MyService getService(String id)})
40 * and creates a dynamic proxy which implements that interface, delegating to an
41 * underlying {@link org.springframework.beans.factory.BeanFactory}.
42 *
43 * <p>Such service locators permit the decoupling of calling code from
44 * the {@link org.springframework.beans.factory.BeanFactory} API, by using an
45 * appropriate custom locator interface. They will typically be used for
46 * <b>prototype beans</b>, i.e. for factory methods that are supposed to
47 * return a new instance for each call. The client receives a reference to the
48 * service locator via setter or constructor injection, to be able to invoke
49 * the locator's factory methods on demand. <b>For singleton beans, direct
50 * setter or constructor injection of the target bean is preferable.</b>
51 *
52 * <p>On invocation of the no-arg factory method, or the single-arg factory
53 * method with a String id of {@code null} or empty String, if exactly
54 * <b>one</b> bean in the factory matches the return type of the factory
55 * method, that bean is returned, otherwise a
56 * {@link org.springframework.beans.factory.NoSuchBeanDefinitionException}
57 * is thrown.
58 *
59 * <p>On invocation of the single-arg factory method with a non-null (and
60 * non-empty) argument, the proxy returns the result of a
61 * {@link org.springframework.beans.factory.BeanFactory#getBean(String)} call,
62 * using a stringified version of the passed-in id as bean name.
63 *
64 * <p>A factory method argument will usually be a String, but can also be an
65 * int or a custom enumeration type, for example, stringified via
66 * {@code toString}. The resulting String can be used as bean name as-is,
67 * provided that corresponding beans are defined in the bean factory.
68 * Alternatively, {@linkplain #setServiceMappings(java.util.Properties) a custom
69 * mapping} between service IDs and bean names can be defined.
70 *
71 * <p>By way of an example, consider the following service locator interface.
72 * Note that this interface is not dependent on any Spring APIs.
73 *
74 * <pre class="code">package a.b.c;
75 *
76 *public interface ServiceFactory {
77 *
78 * public MyService getService();
79 *}</pre>
80 *
81 * <p>A sample config in an XML-based
82 * {@link org.springframework.beans.factory.BeanFactory} might look as follows:
83 *
84 * <pre class="code"><beans>
85 *
86 * <!-- Prototype bean since we have state -->
87 * <bean id="myService" class="a.b.c.MyService" singleton="false"/>
88 *
89 * <!-- will lookup the above 'myService' bean by *TYPE* -->
90 * <bean id="myServiceFactory"
91 * class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
92 * <property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
93 * </bean>
94 *
95 * <bean id="clientBean" class="a.b.c.MyClientBean">
96 * <property name="myServiceFactory" ref="myServiceFactory"/>
97 * </bean>
98 *
99 *</beans></pre>
100 *
101 * <p>The attendant {@code MyClientBean} class implementation might then
102 * look something like this:
103 *
104 * <pre class="code">package a.b.c;
105 *
106 *public class MyClientBean {
107 *
108 * private ServiceFactory myServiceFactory;
109 *
110 * // actual implementation provided by the Spring container
111 * public void setServiceFactory(ServiceFactory myServiceFactory) {
112 * this.myServiceFactory = myServiceFactory;
113 * }
114 *
115 * public void someBusinessMethod() {
116 * // get a 'fresh', brand new MyService instance
117 * MyService service = this.myServiceFactory.getService();
118 * // use the service object to effect the business logic...
119 * }
120 *}</pre>
121 *
122 * <p>By way of an example that looks up a bean <b>by name</b>, consider
123 * the following service locator interface. Again, note that this
124 * interface is not dependent on any Spring APIs.
125 *
126 * <pre class="code">package a.b.c;
127 *
128 *public interface ServiceFactory {
129 *
130 * public MyService getService (String serviceName);
131 *}</pre>
132 *
133 * <p>A sample config in an XML-based
134 * {@link org.springframework.beans.factory.BeanFactory} might look as follows:
135 *
136 * <pre class="code"><beans>
137 *
138 * <!-- Prototype beans since we have state (both extend MyService) -->
139 * <bean id="specialService" class="a.b.c.SpecialService" singleton="false"/>
140 * <bean id="anotherService" class="a.b.c.AnotherService" singleton="false"/>
141 *
142 * <bean id="myServiceFactory"
143 * class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
144 * <property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
145 * </bean>
146 *
147 * <bean id="clientBean" class="a.b.c.MyClientBean">
148 * <property name="myServiceFactory" ref="myServiceFactory"/>
149 * </bean>
150 *
151 *</beans></pre>
152 *
153 * <p>The attendant {@code MyClientBean} class implementation might then
154 * look something like this:
155 *
156 * <pre class="code">package a.b.c;
157 *
158 *public class MyClientBean {
159 *
160 * private ServiceFactory myServiceFactory;
161 *
162 * // actual implementation provided by the Spring container
163 * public void setServiceFactory(ServiceFactory myServiceFactory) {
164 * this.myServiceFactory = myServiceFactory;
165 * }
166 *
167 * public void someBusinessMethod() {
168 * // get a 'fresh', brand new MyService instance
169 * MyService service = this.myServiceFactory.getService("specialService");
170 * // use the service object to effect the business logic...
171 * }
172 *
173 * public void anotherBusinessMethod() {
174 * // get a 'fresh', brand new MyService instance
175 * MyService service = this.myServiceFactory.getService("anotherService");
176 * // use the service object to effect the business logic...
177 * }
178 *}</pre>
179 *
180 * <p>See {@link ObjectFactoryCreatingFactoryBean} for an alternate approach.
181 *
182 * @author Colin Sampaleanu
183 * @author Juergen Hoeller
184 * @since 1.1.4
185 * @see #setServiceLocatorInterface
186 * @see #setServiceMappings
187 * @see ObjectFactoryCreatingFactoryBean
188 */
189 public class ServiceLocatorFactoryBean implements FactoryBean<Object>, BeanFactoryAware, InitializingBean {
190
191 private Class<?> serviceLocatorInterface;
192
193 private Constructor<Exception> serviceLocatorExceptionConstructor;
194
195 private Properties serviceMappings;
196
197 private ListableBeanFactory beanFactory;
198
199 private Object proxy;
200
201
202 /**
203 * Set the service locator interface to use, which must have one or more methods with
204 * the signatures {@code MyType xxx()} or {@code MyType xxx(MyIdType id)}
205 * (typically, {@code MyService getService()} or {@code MyService getService(String id)}).
206 * See the {@link ServiceLocatorFactoryBean class-level Javadoc} for
207 * information on the semantics of such methods.
208 */
209 public void setServiceLocatorInterface(Class<?> interfaceType) {
210 this.serviceLocatorInterface = interfaceType;
211 }
212
213 /**
214 * Set the exception class that the service locator should throw if service
215 * lookup failed. The specified exception class must have a constructor
216 * with one of the following parameter types: {@code (String, Throwable)}
217 * or {@code (Throwable)} or {@code (String)}.
218 * <p>If not specified, subclasses of Spring's BeansException will be thrown,
219 * for example NoSuchBeanDefinitionException. As those are unchecked, the
220 * caller does not need to handle them, so it might be acceptable that
221 * Spring exceptions get thrown as long as they are just handled generically.
222 * @see #determineServiceLocatorExceptionConstructor
223 * @see #createServiceLocatorException
224 */
225 public void setServiceLocatorExceptionClass(Class<? extends Exception> serviceLocatorExceptionClass) {
226 if (serviceLocatorExceptionClass != null && !Exception.class.isAssignableFrom(serviceLocatorExceptionClass)) {
227 throw new IllegalArgumentException(
228 "serviceLocatorException [" + serviceLocatorExceptionClass.getName() + "] is not a subclass of Exception");
229 }
230 this.serviceLocatorExceptionConstructor =
231 determineServiceLocatorExceptionConstructor(serviceLocatorExceptionClass);
232 }
233
234 /**
235 * Set mappings between service ids (passed into the service locator)
236 * and bean names (in the bean factory). Service ids that are not defined
237 * here will be treated as bean names as-is.
238 * <p>The empty string as service id key defines the mapping for {@code null} and
239 * empty string, and for factory methods without parameter. If not defined,
240 * a single matching bean will be retrieved from the bean factory.
241 * @param serviceMappings mappings between service ids and bean names,
242 * with service ids as keys as bean names as values
243 */
244 public void setServiceMappings(Properties serviceMappings) {
245 this.serviceMappings = serviceMappings;
246 }
247
248 @Override
249 public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
250 if (!(beanFactory instanceof ListableBeanFactory)) {
251 throw new FatalBeanException(
252 "ServiceLocatorFactoryBean needs to run in a BeanFactory that is a ListableBeanFactory");
253 }
254 this.beanFactory = (ListableBeanFactory) beanFactory;
255 }
256
257 @Override
258 public void afterPropertiesSet() {
259 if (this.serviceLocatorInterface == null) {
260 throw new IllegalArgumentException("Property 'serviceLocatorInterface' is required");
261 }
262
263 // Create service locator proxy.
264 this.proxy = Proxy.newProxyInstance(
265 this.serviceLocatorInterface.getClassLoader(),
266 new Class<?>[] {this.serviceLocatorInterface},
267 new ServiceLocatorInvocationHandler());
268 }
269
270
271 /**
272 * Determine the constructor to use for the given service locator exception
273 * class. Only called in case of a custom service locator exception.
274 * <p>The default implementation looks for a constructor with one of the
275 * following parameter types: {@code (String, Throwable)}
276 * or {@code (Throwable)} or {@code (String)}.
277 * @param exceptionClass the exception class
278 * @return the constructor to use
279 * @see #setServiceLocatorExceptionClass
280 */
281 @SuppressWarnings("unchecked")
282 protected Constructor<Exception> determineServiceLocatorExceptionConstructor(Class<? extends Exception> exceptionClass) {
283 try {
284 return (Constructor<Exception>) exceptionClass.getConstructor(new Class<?>[] {String.class, Throwable.class});
285 }
286 catch (NoSuchMethodException ex) {
287 try {
288 return (Constructor<Exception>) exceptionClass.getConstructor(new Class<?>[] {Throwable.class});
289 }
290 catch (NoSuchMethodException ex2) {
291 try {
292 return (Constructor<Exception>) exceptionClass.getConstructor(new Class<?>[] {String.class});
293 }
294 catch (NoSuchMethodException ex3) {
295 throw new IllegalArgumentException(
296 "Service locator exception [" + exceptionClass.getName() +
297 "] neither has a (String, Throwable) constructor nor a (String) constructor");
298 }
299 }
300 }
301 }
302
303 /**
304 * Create a service locator exception for the given cause.
305 * Only called in case of a custom service locator exception.
306 * <p>The default implementation can handle all variations of
307 * message and exception arguments.
308 * @param exceptionConstructor the constructor to use
309 * @param cause the cause of the service lookup failure
310 * @return the service locator exception to throw
311 * @see #setServiceLocatorExceptionClass
312 */
313 protected Exception createServiceLocatorException(Constructor<Exception> exceptionConstructor, BeansException cause) {
314 Class<?>[] paramTypes = exceptionConstructor.getParameterTypes();
315 Object[] args = new Object[paramTypes.length];
316 for (int i = 0; i < paramTypes.length; i++) {
317 if (paramTypes[i].equals(String.class)) {
318 args[i] = cause.getMessage();
319 }
320 else if (paramTypes[i].isInstance(cause)) {
321 args[i] = cause;
322 }
323 }
324 return BeanUtils.instantiateClass(exceptionConstructor, args);
325 }
326
327
328 @Override
329 public Object getObject() {
330 return this.proxy;
331 }
332
333 @Override
334 public Class<?> getObjectType() {
335 return this.serviceLocatorInterface;
336 }
337
338 @Override
339 public boolean isSingleton() {
340 return true;
341 }
342
343
344 /**
345 * Invocation handler that delegates service locator calls to the bean factory.
346 */
347 private class ServiceLocatorInvocationHandler implements InvocationHandler {
348
349 @Override
350 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
351 if (ReflectionUtils.isEqualsMethod(method)) {
352 // Only consider equal when proxies are identical.
353 return (proxy == args[0]);
354 }
355 else if (ReflectionUtils.isHashCodeMethod(method)) {
356 // Use hashCode of service locator proxy.
357 return System.identityHashCode(proxy);
358 }
359 else if (ReflectionUtils.isToStringMethod(method)) {
360 return "Service locator: " + serviceLocatorInterface.getName();
361 }
362 else {
363 return invokeServiceLocatorMethod(method, args);
364 }
365 }
366
367 private Object invokeServiceLocatorMethod(Method method, Object[] args) throws Exception {
368 Class<?> serviceLocatorMethodReturnType = getServiceLocatorMethodReturnType(method);
369 try {
370 String beanName = tryGetBeanName(args);
371 if (StringUtils.hasLength(beanName)) {
372 // Service locator for a specific bean name
373 return beanFactory.getBean(beanName, serviceLocatorMethodReturnType);
374 }
375 else {
376 // Service locator for a bean type
377 return beanFactory.getBean(serviceLocatorMethodReturnType);
378 }
379 }
380 catch (BeansException ex) {
381 if (serviceLocatorExceptionConstructor != null) {
382 throw createServiceLocatorException(serviceLocatorExceptionConstructor, ex);
383 }
384 throw ex;
385 }
386 }
387
388 /**
389 * Check whether a service id was passed in.
390 */
391 private String tryGetBeanName(Object[] args) {
392 String beanName = "";
393 if (args != null && args.length == 1 && args[0] != null) {
394 beanName = args[0].toString();
395 }
396 // Look for explicit serviceId-to-beanName mappings.
397 if (serviceMappings != null) {
398 String mappedName = serviceMappings.getProperty(beanName);
399 if (mappedName != null) {
400 beanName = mappedName;
401 }
402 }
403 return beanName;
404 }
405
406 private Class<?> getServiceLocatorMethodReturnType(Method method) throws NoSuchMethodException {
407 Class<?>[] paramTypes = method.getParameterTypes();
408 Method interfaceMethod = serviceLocatorInterface.getMethod(method.getName(), paramTypes);
409 Class<?> serviceLocatorReturnType = interfaceMethod.getReturnType();
410
411 // Check whether the method is a valid service locator.
412 if (paramTypes.length > 1 || void.class.equals(serviceLocatorReturnType)) {
413 throw new UnsupportedOperationException(
414 "May only call methods with signature '<type> xxx()' or '<type> xxx(<idtype> id)' " +
415 "on factory interface, but tried to call: " + interfaceMethod);
416 }
417 return serviceLocatorReturnType;
418 }
419 }
420
421 }